package com.hra.sittingposturedetect;

import static android.hardware.Camera.CameraInfo.CAMERA_FACING_FRONT;

import android.Manifest;
import android.annotation.TargetApi;
import android.app.AlertDialog;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.ImageFormat;
import android.graphics.Matrix;
import android.graphics.drawable.ColorDrawable;
import android.hardware.Camera;
import android.hardware.camera2.CameraAccessException;
import android.hardware.camera2.CameraCaptureSession;
import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.CameraDevice;
import android.hardware.camera2.CameraManager;
import android.hardware.camera2.CaptureRequest;
import android.hardware.camera2.TotalCaptureResult;
import android.hardware.camera2.params.StreamConfigurationMap;
import android.media.Image;
import android.media.ImageReader;
import android.os.Binder;
import android.os.Build;
import android.os.FileUtils;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.IBinder;
import android.os.Looper;
import android.text.TextUtils;
import android.util.DisplayMetrics;
import android.util.Log;
import android.util.Range;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.Surface;
import android.view.SurfaceHolder;
import android.util.Size;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.LinearLayout;
import android.widget.RelativeLayout;
import android.widget.TextView;
import android.widget.Toast;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.core.app.ActivityCompat;
import androidx.core.app.NotificationCompat;

import com.hra.sittingposturedetect.R;
import com.example.sittingposture.Recognition;
import com.example.sittingposture.Yolov5TFLiteDetector;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

public class CameraService extends Service {
    private static final String TAG = "CameraService";

    // service notification
    private static final int NOTIFICATION_ID = 1;
    private static final String CHANNEL_ID = "CameraServiceChannel";

    // yolov
    private Yolov5TFLiteDetector yolov5TFLiteDetector;
    private ArrayList<Recognition> mRecognitions = null;

    private String mCameraID = "";
    private CameraManager mCameraManager;
    private CameraDevice mCameraDevice;
    private CameraCharacteristics mCharacteristics = null;
    private CaptureRequest.Builder mCaptureRequestBuilder;
    private ImageReader mCaptureImageReader;
    private static final int TYPE_PREVIEW = 1;
    private static final int TYPE_CAPTURE = 2;

    // CaptureImageReader onImageAvailable sample setting
    private static final int SAMPLING_FRAME_COUNT = 60; //fps
    private boolean mProcessingFlag = false;
    private int mCount = 0;

    private final HandlerThread handlerThread = new HandlerThread("CameraServiceThread");
    private Handler backgroundHandler;

    private Handler mainHandler = new Handler(Looper.getMainLooper());

    private static final long mKeepCameraPollingMilis = 10*1000;

    private AlertDialog mRecogResultDialog;
    private AlertDialog.Builder mDialogBuilder;
    private TextView mTvRecogResult;

    private Runnable mTimeCounterRunnable = new Runnable() {
        @Override
        public void run() {
            if (mCameraDevice == null && checkCameraFacing(CAMERA_FACING_FRONT)) {
                openCamera();
            }
            mainHandler.postDelayed(this, mKeepCameraPollingMilis);
        }
    };

    @Override
    public void onCreate() {
        super.onCreate();

        Log.i(TAG, "onCreate");

        initDetect();

        // set mCameraID
        mCameraManager = (CameraManager) getSystemService(CAMERA_SERVICE);
        try {
            String[] cameraIdList = mCameraManager.getCameraIdList();
            boolean hasFrontCamera = false;

            for (String cameraId : cameraIdList) {
                CameraCharacteristics ctics = mCameraManager.getCameraCharacteristics(cameraId);
                int facing = ctics.get(CameraCharacteristics.LENS_FACING);

                if (facing == CameraCharacteristics.LENS_FACING_FRONT) {
                    hasFrontCamera = true;
                    mCameraID = cameraId;
                    Log.i(TAG, "have front camera id="+cameraId);
                    break;
                }
            }
            if (!hasFrontCamera) {
                Log.i(TAG, "have not front camera");
            }
        } catch (CameraAccessException e) {
            throw new RuntimeException(e);
        }


        handlerThread.start();
        backgroundHandler = new Handler(handlerThread.getLooper());

        createNotificationChannel();
        startForeground(NOTIFICATION_ID, createNotification());
    }

    @RequiresApi(api = Build.VERSION_CODES.O)
    private void createNotificationChannel() {
        Log.i(TAG, "createNotificationChannel start");
        NotificationChannel serviceChannel = new NotificationChannel(
                CHANNEL_ID,
                "Camera Service Channel",
                NotificationManager.IMPORTANCE_DEFAULT
        );
        NotificationManager manager = getSystemService(NotificationManager.class);
        manager.createNotificationChannel(serviceChannel);

        Log.i(TAG, "createNotificationChannel end");
    }

    private Notification createNotification() {
        Intent notificationIntent = new Intent(this, MainActivity.class);
        PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, notificationIntent, PendingIntent.FLAG_IMMUTABLE);

        return new NotificationCompat.Builder(this, CHANNEL_ID)
                .setContentTitle("Camera Service")
                .setContentText("Running")
                .setSmallIcon(R.drawable.ic_launcher_background)
                .setContentIntent(pendingIntent)
                .build();
    }

    private void openCamera() {
        Log.i(TAG, "openCamera start mCameraID="+mCameraID);

        if (TextUtils.isEmpty(mCameraID)) {
            Log.i(TAG, "openCamera failed because of having none valid mCameraID");
            return;
        }

        try {
            mCharacteristics = mCameraManager.getCameraCharacteristics(mCameraID);
            Log.i(TAG, "openCamera mCharacteristics.get(CameraCharacteristics.LENS_FACING)="+mCharacteristics.get(CameraCharacteristics.LENS_FACING));

            if (ActivityCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
                Log.i(TAG, "openCamera return because of having not camera permission");
                return;
            }
            mCameraManager.openCamera(mCameraID, stateCallback, backgroundHandler);
        } catch (CameraAccessException e) {
            e.printStackTrace();
        }
    }

    private final CameraDevice.StateCallback stateCallback = new CameraDevice.StateCallback() {
        @Override
        public void onOpened(@NonNull CameraDevice camera) {
            Log.i(TAG, "CameraDevice stateCallback onOpened camera=" + camera.toString());
            mCameraDevice = camera;

            setCaptureImageReader();
            takePreview();
        }

        @Override
        public void onDisconnected(@NonNull CameraDevice camera) {
            Log.i(TAG, "CameraDevice stateCallback onDisconnected camera=" + camera.toString());
            if (mCameraDevice != null) {
                mCameraDevice.close();
                mCameraDevice = null;
            }

            if (mCaptureImageReader != null) {
                mCaptureImageReader.close();
                mCaptureImageReader = null;
            }
        }

        @Override
        public void onError(@NonNull CameraDevice camera, int error) {
            Log.i(TAG, "CameraDevice stateCallback onError camera=" + camera.toString() + ", error=" + error);
            if (mCameraDevice != null) {
                mCameraDevice.close();
                mCameraDevice = null;
            }
            //camera.unlock();

            if (mCaptureImageReader != null) {
                mCaptureImageReader.close();
                mCaptureImageReader = null;
            }
        }
    };

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        mainHandler.post(mTimeCounterRunnable);
        openCamera();

        Log.i(TAG, "onStartCommand intent=" + intent);
        return START_STICKY;
    }



    @Override
    public void onDestroy() {
        super.onDestroy();

        mProcessingFlag = false;

        mainHandler.removeCallbacks(mTimeCounterRunnable);

        handlerThread.quitSafely();
        if (mCameraDevice != null) {
            mCameraDevice.close();
            mCameraDevice = null;
        }
    }

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    private static boolean checkCameraFacing(final int facing) {
        final int cameraCount = Camera.getNumberOfCameras();
        Camera.CameraInfo info = new Camera.CameraInfo();
        for (int i = 0; i < cameraCount; i++) {
            Camera.getCameraInfo(i, info);
            if (facing == info.facing) {
                boolean canUse = true;
                Camera camera = null;
                try {
                    camera = Camera.open(facing);
                } catch (RuntimeException e) {
                    canUse = false;
                    Log.i(TAG, "checkCameraFacing Camera.open canUse=" + canUse + ", exception=" + e.toString());
                } finally {
                    if (camera != null) {
                        camera.release();
                    }
                }
                Log.i(TAG, "checkCameraFacing camera canUse=" + canUse);
                return canUse;
            }
        }
        return false;
    }

    static class CompareSizesByArea implements Comparator<Size> {
        @Override
        public int compare(Size lhs, Size rhs) {
            // use Long forced to avoid the result overflow.
            return Long.signum((long) lhs.getWidth() * lhs.getHeight() -
                    (long) rhs.getWidth() * rhs.getHeight());
        }
    }

    public int getScreenWidth() {
        WindowManager wm = (WindowManager) getSystemService(Context.WINDOW_SERVICE);
        DisplayMetrics outMetrics = new DisplayMetrics();
        wm.getDefaultDisplay().getRealMetrics(outMetrics);
        return outMetrics.widthPixels;
    }

    public int getScreenHeight() {
        WindowManager wm = (WindowManager) getSystemService(Context.WINDOW_SERVICE);
        DisplayMetrics outMetrics = new DisplayMetrics();
        wm.getDefaultDisplay().getRealMetrics(outMetrics);
        return outMetrics.heightPixels;
    }

    private void setCaptureImageReader() {
        if (mCharacteristics == null) {
            Log.w(TAG, "setCaptureImageReader fail because of mCharacteristics == null");
            return;
        }

        StreamConfigurationMap map = mCharacteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
        Size suitableJpegSize = chooseOptimalSizeForScreen(map.getOutputSizes(ImageFormat.JPEG), 3, 4, TYPE_CAPTURE);

        Log.w(TAG, "setCaptureImageReader suitableJpegSize=" + suitableJpegSize);

        mCaptureImageReader = ImageReader.newInstance(suitableJpegSize.getWidth(), suitableJpegSize.getHeight(), ImageFormat.JPEG/*ImageFormat.YUV_420_888*/, 2);
        mCaptureImageReader.setOnImageAvailableListener(new ImageReader.OnImageAvailableListener() {
            @TargetApi(Build.VERSION_CODES.LOLLIPOP)
            @Override
            public void onImageAvailable(ImageReader reader) {
                Log.i(TAG, "mCaptureImageReader onImageAvailable callback time: " + System.currentTimeMillis());

                Image image = null;

                try {
                    image = reader.acquireNextImage();

                    if (image != null) {
                        mCount++;

                        if (mCount >= SAMPLING_FRAME_COUNT && !mProcessingFlag) {
                            Log.w(TAG, "image width/height:" + image.getWidth() + "/" + image.getHeight());
                            long currentTime = System.currentTimeMillis();

                            Log.w(TAG, "startScan currentTime=" + currentTime);

                            mProcessingFlag = true;
                            ByteBuffer buffer = image.getPlanes()[0].getBuffer();
                            byte[] bytes = new byte[buffer.capacity()];
                            buffer.get(bytes);
                            Bitmap bitmapImage = BitmapFactory.decodeByteArray(bytes, 0, bytes.length, null);
                            //Bitmap imageBitmap = BitmapUtil.rotateBitmap(temp, 90);
                            //Bitmap rotatedBitmap = Bitmap.createBitmap(bitmapImage, 0, 0, bitmapImage.getWidth(), bitmapImage.getHeight(), m, true);

                            // start yolov detect
                            mRecognitions = yolov5TFLiteDetector.detect(bitmapImage);
                            if (mRecognitions != null && !mRecognitions.isEmpty()) {
                                String result = "";
                                Log.d(TAG, "detect succeed");
                                for (Recognition res : mRecognitions) {
                                    result = res.getLabelName() + ", " + res.getConfidence();
                                    Log.d(TAG, "recognitions=" + result);
                                    //Toast.makeText(CameraService.this, result, Toast.LENGTH_SHORT).show();
                                }

                                if (!TextUtils.isEmpty(result)) {
                                    showDialog(result);
                                }

                                // test:  bitmap check
                                String saveSwitch = getProp("persist.sys.save_detect_bitmap");
                                if (!TextUtils.isEmpty(saveSwitch)
                                        && saveSwitch.equals("yes")) {
                                    saveBitmap("bitmap_for_detect_"+currentTime+"_"+result+".jpg", bitmapImage, CameraService.this);
                                }
                            } else {
                                Log.d(TAG, "detect fail");
                            }
                            mProcessingFlag = false;
                            mCount = 0;
                        }
                    }
                } catch (IllegalStateException e) {
                    Log.e(TAG, e.getMessage());
                    return;
                } finally {
                    if (null != image) {
                        image.close();
                    }
                }
            }
        }, mainHandler);
    }

    private Size chooseOptimalSizeForScreen(Size[] choices, int width, int height, int type) {
        List<Size> suitableSize = new ArrayList<>();
        float aspectRatio = (float) height / width;
        Log.w(TAG, "chooseOptimalSizeForScreen input : " + width + "/" + height + ", aspectRatio = " + aspectRatio);

        int screenWidth = getScreenWidth();
        int screenHeight = getScreenHeight();
        Log.w(TAG, "chooseOptimalSizeForScreen screenWidth: " + screenWidth + "/" + screenHeight);

        for (Size option : choices) {
            Log.w(TAG, "TYPE_PREVIEW choices option=" + option.getWidth() + ", height=" + option.getHeight());

            if ((float) option.getWidth() / option.getHeight() == aspectRatio) {
                /*if (option.getHeight() >= screenWidth) */{
                    suitableSize.add(option);
                    Log.w(TAG, "TYPE_PREVIEW suitableSize width=" + option.getWidth() + ", height=" + option.getHeight());
                }
            }
        }

        if (suitableSize.size() > 0) {
            return Collections.min(suitableSize, new CompareSizesByArea());
        } else {
            Log.e(TAG, "Couldn't find any suitable preview size");
            return choices[0];
        }
    }

    /**
     * start the preview.
     */
    private void takePreview() {
        try {
            mCaptureRequestBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE);
            mCaptureRequestBuilder.addTarget(mCaptureImageReader.getSurface());
            mCaptureRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
            mCaptureRequestBuilder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH);

            List<Surface> surfaceList = new ArrayList<>();
            surfaceList.add(mCaptureImageReader.getSurface());

            mCameraDevice.createCaptureSession(surfaceList,
                    new CameraCaptureSession.StateCallback() {
                        @Override
                        public void onConfigured(CameraCaptureSession cameraCaptureSession) {
                            if (null == mCameraDevice) {
                                return;
                            }

                            // start the preview right now.
                            try {
                                cameraCaptureSession.setRepeatingRequest(mCaptureRequestBuilder.build(), null, null);
                            } catch (CameraAccessException e) {
                                e.printStackTrace();
                            }
                        }

                        @Override
                        public void onConfigureFailed(@NonNull CameraCaptureSession session) {

                        }
                    }, null);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private void initDetect() {
        // 默认加载5n(目前只使用此模型)
        try {
            this.yolov5TFLiteDetector = new Yolov5TFLiteDetector();
            this.yolov5TFLiteDetector.initialModel(this);
            Log.i(TAG, "Success loading model" + this.yolov5TFLiteDetector.getModelFile());
        } catch (Exception e) {
            Log.e(TAG, "load model error: " + e.getMessage() + e);
        }
    }

    static void saveBitmap(String name, Bitmap bm, Context mContext) {
        Log.d(TAG, "Ready to save picture");
        //指定我们想要存储文件的地址
        String TargetPath = mContext.getFilesDir() + "/images/";
        Log.d(TAG, "Save Path=" + TargetPath);
        //判断指定文件夹的路径是否存在
        if (!fileIsExist(TargetPath)) {
            Log.d(TAG, "TargetPath isn't exist");
        } else {
            //如果指定文件夹创建成功，那么我们则需要进行图片存储操作
            File saveFile = new File(TargetPath, name);

            try {
                FileOutputStream saveImgOut = new FileOutputStream(saveFile);
                // compress - 压缩的意思
                bm.compress(Bitmap.CompressFormat.JPEG, 80, saveImgOut);
                //存储完成后需要清除相关的进程
                saveImgOut.flush();
                saveImgOut.close();
                Log.d(TAG, "The picture is save to your phone!");
            } catch (IOException ex) {
                ex.printStackTrace();
            }
        }
    }

    private static boolean fileIsExist(String fileName)
    {
        //传入指定的路径，然后判断路径是否存在
        File file=new File(fileName);
        if (file.exists())
            return true;
        else{
            //file.mkdirs() 创建文件夹的意思
            return file.mkdirs();
        }
    }

    public static String getProp(String key) {
        String result="";
        try {
            Class<?> c = Class.forName("android.os.SystemProperties");

            Method get = c.getMethod("get", String.class);
            result=(String)get.invoke(c, key);

        } catch (ClassNotFoundException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (IllegalArgumentException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        return result;
    }

    private void showDialog(String msg) {
        if (mRecogResultDialog == null) {
            if (mDialogBuilder == null) {
                mDialogBuilder = new AlertDialog.Builder(this, R.style.style_dialog);

                View view = LayoutInflater.from(this).inflate(R.layout.dialog_recog_result, null);
                mTvRecogResult = view.findViewById(R.id.tv_result);
                mDialogBuilder.setView(view);
            }

            mRecogResultDialog = mDialogBuilder.create();
            mRecogResultDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT);
            mRecogResultDialog.getWindow().setGravity(Gravity.CENTER);
            mRecogResultDialog.getWindow().setLayout(getScreenWidth()/5, getScreenHeight()/5);
            mRecogResultDialog.getWindow().setBackgroundDrawable(new ColorDrawable(0));
            mRecogResultDialog.setCanceledOnTouchOutside(true);
        } else {
            if (mRecogResultDialog.isShowing()) {
                mRecogResultDialog.dismiss();
            }
        }

        mTvRecogResult.setText(msg);
        mRecogResultDialog.show();

        new Handler().postDelayed(new Runnable() {
            @Override
            public void run() {
                mRecogResultDialog.dismiss();
            }
        }, 1000);
    }
}